package edu.gatech.oad.mule.model;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

import javax.swing.JOptionPane;
import javax.swing.Timer;

import edu.gatech.oad.mule.Bar;
import edu.gatech.oad.mule.Exit;
import edu.gatech.oad.mule.Game;
import edu.gatech.oad.mule.Map;
import edu.gatech.oad.mule.Player;
import edu.gatech.oad.mule.RandomEvent;
import edu.gatech.oad.mule.RandomEvent.eventConstraint;
import edu.gatech.oad.mule.Store;
import edu.gatech.oad.mule.Tile;
import edu.gatech.oad.mule.TownMap;
import edu.gatech.oad.mule.WorldMap;
import edu.gatech.oad.mule.Zone;
import edu.gatech.oad.mule.config.interfaces._ConfigurationPanel;
import edu.gatech.oad.mule.input.InputEvent;
import edu.gatech.oad.mule.input.InputEvent.ButtonPress;
import edu.gatech.oad.mule.input.interfaces.ControllerOwner;
import edu.gatech.oad.mule.input.interfaces.InputListener;
import edu.gatech.oad.mule.model.GameStateMachine.GameState;
import edu.gatech.oad.mule.view.View;
import edu.gatech.oad.mule.view.interfaces.RenderableGameDriver;
import edu.gatech.oad.mule.view.interfaces.RenderableMap;
import edu.gatech.oad.mule.view.interfaces.RenderablePlayer;
import edu.gatech.oad.mule.view.interfaces.RenderableTile;
import edu.gatech.oad.mule.view.interfaces.RenderableTile.TileType;

/**
 * This class represents the game engine. It has all the logic and serves as a
 * puppet master, controlling all the pieces of the game, handling interactions
 * between classes, etc.
 * 
 * @author Brendan McGarry, Dale Avery, D'Miria Collins, Natasha Parekh, Jaemin
 *         Park
 * @version %I%, %G%
 */
public final class GameDriver extends GameStateMachine implements
		InputListener, RenderableGameDriver {

	/**
	 * 
	 */
	private static final long serialVersionUID = 2966900843716161687L;

	private static final String SAVE_EXTENSION = ".sav";

	private static final int DELAY_INTRO = 3000;
	private static final int DELAY_PLAYER_RANKING = 3000;
	private static final int DELAY_LAND_GRANT_TURN = 10000;
	private static final int DELAY_DEVELOPMENT_TURN = DELAY_LAND_GRANT_TURN;

	private static final int PLAYER_MOVEMENT_SPEED = 5;

	GameState nextState;
	String difficulty;
	Game game;
	Timer timer;
	private int timeCounter;

	// Values for land grant phase
	private ArrayList<Player> players;
	private int currentTurnLength;
	private Player currentPlayer;
	private Map currentMap;

	/**
	 * This constructor configures a new GameDriver object
	 */
	public GameDriver(_ConfigurationPanel panel) {
		System.out.println("gameDriver");

		// Obtain data related to setting up game though config panel
		state = GameState.INTRO;
		difficulty = panel.getDifficulty();
		game = new Game(panel);

		// Initialize timer for controlling turns
		timer = new Timer(50, null);

		System.out.println("/gameDriver");
	}// end constructor

	/**
	 * Test constructor. Just builds a GameDriver and starts the timer.
	 */
	public GameDriver() {
		super(GameState.ERROR);
		System.out.println("gameDriverTest");

		timer = new Timer(50, new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent arg0) {
				gameLoop();
			}// end method actionPerformed
		});
		// timer.start();
		System.out.println("/gameDriverTest");
	}// end constructor

	/**
	 * Intended to be the method that initiates the game, not really sure how to
	 * implement game loop right now.
	 */
	public void gameLoop() {

		// main game loop should:
		// 1) update game
		// 2) render the game
		// 3) repaint

		// System.out.println("startGame");
		boolean playing = true;

		if (nextState == null) {
			switch (state) {
			case INTRO:
				introPhase();
				break;
			case PLAYER_RANKING:
				playerRankingPhase();
				break;
			case LAND_GRANT:
				landGrantPhase();
				break;
			case DEVELOPMENT:
				developmentPhase();
				break;
			}// end switch
		} else {
			// Run setup methods on any phases that require one.
			switch (nextState) {
			case INTRO:
				state = GameState.INTRO;
				break;
			case PLAYER_RANKING:
				state = GameState.PLAYER_RANKING;
				break;
			case LAND_GRANT:
				setupLandGrantPhase();
				state = GameState.LAND_GRANT;
				break;
			case DEVELOPMENT:
				setupDevelopmentPhase();
				state = GameState.DEVELOPMENT;
				break;
			default:
				break;
			}// end switch
			changeState(nextState);
			nextState = null;
		}// end else
			// System.out.println("/startGame");
	}// end method startGame

	/**
	 * Keeps the game in the intro phase for a specific period of time.
	 */
	private void introPhase() {
		timeCounter += timer.getDelay();
		// Transition to next phase after intro delay.
		if (timeCounter >= DELAY_INTRO) {
			nextState = GameState.PLAYER_RANKING;
			System.out.println("Intro phase lasted " + timeCounter
					+ " milliseconds.  Transitioning to player ranking phase.");
			timeCounter = 0;
		}// end if
	}// end method introPhase

	/**
	 * Keeps the game in the ranking phase for a specific period of time.
	 */
	private void playerRankingPhase() {
		timeCounter += timer.getDelay();
		// Transition to next phase after ranking delay
		if (timeCounter >= DELAY_PLAYER_RANKING) {
			nextState = GameState.LAND_GRANT;
			System.out.println("Player ranking phase lasted " + timeCounter
					+ " milliseconds.  Transitioning to land grant phase.");
			timeCounter = 0;
		}// end if
	}// end method playerRankingPhase

	/**
	 * Sets up a land grant phase by obtaining a ranked list of players
	 */
	private void setupLandGrantPhase() {
		currentMap = game.getWorldMap();
		if (currentMap == null)
			System.out.println("Map is null going into land grant phase!");
		players = new ArrayList<Player>(Arrays.asList(game.getRankedPlayers()));
		// Land grant phase moves from the lowest rank to the greatest
		currentPlayer = players.remove(0);
		currentPlayer.setLocation(WorldMap.WIDTH / 2, WorldMap.HEIGHT / 2);
		currentTurnLength = DELAY_LAND_GRANT_TURN;
		System.out.println("Land grant phase for: " + currentPlayer.toString());
		timeCounter = 0;
		// TODO instead of iterating through players, should iterate through
		// tiles instead.
	}// end method setupLandGrantPhase

	/**
	 * Keeps the game in the landGrantPhase for a specific period of time.
	 */
	private void landGrantPhase() {
		timeCounter += timer.getDelay();
		// Transition to next phase after ranking delay
		if (timeCounter >= currentTurnLength) {
			if (players != null && !players.isEmpty()) {
				// Land grant phase moves from the lowest rank to the greatest
				currentPlayer = players.remove(0);
				currentPlayer.setLocation(WorldMap.WIDTH / 2,
						WorldMap.HEIGHT / 2);
				currentTurnLength = DELAY_LAND_GRANT_TURN;
				currentMap = game.getWorldMap();
				System.out.println("Land grant phase for: "
						+ currentPlayer.toString());
			} else {
				nextState = GameState.DEVELOPMENT;
				System.out
						.println("Land grant phase completed. Transitioning to development phase.");
			}// end else
			timeCounter = 0;
		}// end if
	}// end method landGrantPhase

	/**
	 * Sets up a development phase by obtaining a ranked list of players
	 */
	private void setupDevelopmentPhase() {
		currentMap = game.getTownMap();
		players = new ArrayList<Player>(Arrays.asList(game.getRankedPlayers()));
		// Development phase moves from the lowest rank to the greatest
		currentPlayer = players.remove(0);
		currentPlayer.setLocation(WorldMap.WIDTH / 2, WorldMap.HEIGHT / 2);
		
		// Apply random event to next player.
		if (currentPlayer.equals(game.getRankedPlayers()[0])) {
			RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.GOOD);
		} else if (currentPlayer.equals(game.getRankedPlayers()[game.getRankedPlayers().length - 1])) {
			RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.BAD);
		} else {
			RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.NONE);
		}
		
		// TODO find the actual length of time a player's turn should be
		if (currentPlayer.getFood() > 3) {
			currentTurnLength = 50000;
		} else {
			currentTurnLength = 5000;
		}
		System.out
				.println("Development phase for: " + currentPlayer.toString());
		timeCounter = 0;
	}// end method setupDevelopmentPhase

	/**
	 * Will represent the logic for keeping the game in a development phase
	 * based on the amount of food each player has
	 */
	private void developmentPhase() {
		timeCounter += timer.getDelay();
		// Transition to next phase after ranking delay
		if (timeCounter >= currentTurnLength) {
			if (players != null && !players.isEmpty()) {
				// Development phase moves from the lowest rank to the greatest
				currentPlayer = players.remove(0);
				currentPlayer.setLocation(WorldMap.WIDTH / 2,
						WorldMap.HEIGHT / 2);
				// TODO find the actual length of time a player's turn should be
				if (currentPlayer.getFood() >= 3) {
					currentTurnLength = 50000;
				} else {
					currentTurnLength = 5000;
				}
				
				// Apply random event to next player.
				if (currentPlayer.equals(game.getRankedPlayers()[0])) {
					RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.GOOD);
				} else if (currentPlayer.equals(game.getRankedPlayers()[game.getRankedPlayers().length - 1])) {
					RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.BAD);
				} else {
					RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.NONE);
				}
				
				currentMap = game.getTownMap();
				System.out.println("Development phase for: "
						+ currentPlayer.toString());
			} else {
				nextState = GameState.PLAYER_RANKING;
				// Player ranking occurs at the start of the round.
				
				System.out.println("Calculating production for round: " + game.getRound());
				((TownMap)game.getTownMap()).production();
				((WorldMap)game.getWorldMap()).production(game.getPlayers());
				game.nextRound();
				System.out
						.println("Development phase completed. Transitioning to ranking phase.");
			}// end else
			timeCounter = 0;
		}// end if
	}// end method developmentPhase

	public void start() {
		System.out.println("Starting GameDriver with state: " + state);
		if (timer.getActionListeners().length == 0) {
			System.out.println("Timer has no listeners.  Adding a new one.");
			timer.addActionListener(newListener());
		}
		if (!timer.isRunning()) {
			changeState(state);
			System.out.println("Starting timer with " + timer.getDelay()
					+ " seconds for a delay.");
			timer.start();
		}// end if
	}// end method start

	private ActionListener newListener() {
		return new ActionListener() {
			@Override
			public void actionPerformed(ActionEvent arg0) {
				gameLoop();
			}// end method actionPerformed
		};
	}

	@Override
	public void inputEventHandler(InputEvent event) {

		if (currentPlayer != null) {
			if (event.getPress() == ButtonPress.UP) {
				currentPlayer.move(0, -PLAYER_MOVEMENT_SPEED);

			} else if (event.getPress() == ButtonPress.DOWN) {
				currentPlayer.move(0, PLAYER_MOVEMENT_SPEED);

			} else if (event.getPress() == ButtonPress.LEFT) {
				currentPlayer.move(-PLAYER_MOVEMENT_SPEED, 0);

			} else if (event.getPress() == ButtonPress.RIGHT) {
				currentPlayer.move(PLAYER_MOVEMENT_SPEED, 0);

			} else if (event.getPress() == ButtonPress.ACTION) {
				System.out
						.println("Player should interact with environment now at "
								+ currentPlayer.getLocation());
				Zone zone = currentMap.getCollidedZone(currentPlayer);
				if (zone != null) {
					if (zone instanceof Tile) {
						Tile tile = (Tile) zone;
						if (tile.getTileType() == TileType.TOWN
								&& state == GameState.DEVELOPMENT) {
							// TODO switch maps to Town Map
							currentMap = game.getTownMap();
							currentPlayer.setLocation(WorldMap.WIDTH / 2,
									WorldMap.HEIGHT / 2);
						} else if (tile.getTileType() == TileType.ERROR) {
							// TODO throw an error for invalid tiles or
							// something.
						} else {
							// TODO calculate round (third parameter)
							tile.interact(currentPlayer, state,
									game.getRound(),
									(currentTurnLength - timeCounter));

							// Prevent players from grabbing more than one tile.
							if (state == GameState.LAND_GRANT) {
								if (players != null && !players.isEmpty()) {
									currentPlayer = players.remove(0);
									currentPlayer.setLocation(
											WorldMap.WIDTH / 2,
											WorldMap.HEIGHT / 2);
								} else {
									nextState = GameState.DEVELOPMENT;
									System.out
											.println("Land Grant phase completed. Transitioning to development phase.");
								}
							}
						}
					} else if (zone instanceof Bar) {
						// Interacting with Bar should end the player's turn and
						// transition to next player's turn
						zone.interact(currentPlayer, state, game.getRound(),
								(currentTurnLength - timeCounter));
						if (players != null && !players.isEmpty()) {
							currentPlayer = players.remove(0);
							currentPlayer.setLocation(WorldMap.WIDTH / 2,
									WorldMap.HEIGHT / 2);
							
							// Apply random event to next player.
							if (currentPlayer.equals(game.getRankedPlayers()[0])) {
								RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.GOOD);
							} else if (currentPlayer.equals(game.getRankedPlayers()[game.getRankedPlayers().length - 1])) {
								RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.BAD);
							} else {
								RandomEvent.distribute(currentPlayer, game.getRound(), eventConstraint.NONE);
							}
						} else {
							nextState = GameState.PLAYER_RANKING;
							// Player ranking occurs at the start of the round.
							game.nextRound();
							System.out.println("Calculating production for round: " + game.getRound());
							((TownMap)game.getTownMap()).production();
							((WorldMap)game.getWorldMap()).production(game.getPlayers());
							System.out
									.println("Development phase completed. Transitioning to ranking phase.");
						}
						// TODO find the actual length of time a player's turn
						// should be
						if (currentPlayer.getFood() >= 3) {
							currentTurnLength = 50000;
						} else {
							currentTurnLength = 5000;
						}
						currentMap = game.getTownMap();
						System.out.println("Development phase for: "
								+ currentPlayer.toString());
						timeCounter = 0;

					} else if (zone instanceof Store) {
						// Interacting with the store should display a GUI for
						// the player to buy and sell resources.
						zone.interact(currentPlayer, state, game.getRound(),
								(currentTurnLength - timeCounter));
					} else if (zone instanceof Exit) {
						// Interacting with an exit should transition to the
						// world map
						zone.interact(currentPlayer, state, game.getRound(),
								(currentTurnLength - timeCounter));
						currentMap = game.getWorldMap();
						currentPlayer.setLocation(WorldMap.WIDTH / 2,
								WorldMap.HEIGHT / 2);
					} else {
						zone.interact(currentPlayer, state, game.getRound(),
								(currentTurnLength - timeCounter));
					}// end else if
				}
			}// end else if
		}// end if
	}// end method inputHandler

	@Override
	public RenderableMap getCurrentMap() {
		return (RenderableMap) currentMap;
	}// end method getCurrentMap

	@Override
	public RenderablePlayer getCurrentPlayer() {
		return currentPlayer;
	}// end method getCurrentPlayer

	@Override
	public RenderablePlayer[] getAllPlayers() {
		return game.getPlayers();
	}// end method getAllPlayers

	@Override
	public ControllerOwner[] getActiveOwners() {
		// TODO Auto-generated method stub
		return null;
	}

	// ====================
	// saving/loading stuffs. TODO
	// ====================

	/**
	 * Loads the saved game using the name of the save file passed in
	 * 
	 * @param savedGD
	 *            The name of the save file
	 * @return Whether or not the game successfully loaded
	 */
	public static GameDriver load(File savedGD) throws IOException,
			ClassNotFoundException {

		FileInputStream fis = new FileInputStream(savedGD);
		ObjectInputStream ois = new ObjectInputStream(fis);
		// load stuff here? This method should probably be static
		// or handled by another class
		GameDriver gd = (GameDriver) ois.readObject();
		ois.close();
		fis.close();
		return gd;
	}// end method

	/**
	 * Saves the current game by serializing GameDriver
	 * 
	 * @return Whether or not the game successfully saved
	 */
	public boolean save() {
		// Pause the game before saving
		timer.stop();
		// name of the save file
		String saveFile = "";
		// keep asking for a save file
		while (!validFileName(saveFile)) {
			saveFile = JOptionPane
					.showInputDialog(
							null,
							"Enter the desired save file name.\nDo NOT add a file extension.",
							"Save Game", JOptionPane.QUESTION_MESSAGE);
		}// end while
		try {
			FileOutputStream fos = new FileOutputStream(saveFile
					+ SAVE_EXTENSION);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(this);
			oos.flush();
			oos.close();
			fos.close();
			JOptionPane.showMessageDialog(null, ("Game successfully saved to ["
					+ (saveFile + SAVE_EXTENSION) + "]!"), "Save Complete!",
					JOptionPane.INFORMATION_MESSAGE);
		}// end try
		catch (IOException e) {
			JOptionPane.showMessageDialog(null,
					("Uh-oh...\n\n--------------------\n" + e.getMessage()
							+ "\n--------------------\n" + e.getStackTrace()),
					"Save Failed", JOptionPane.ERROR_MESSAGE);
			System.out.println("====================");
			System.out.println("\tSAVE FAILED");
			System.out.println("====================");
			System.out.println(e.getMessage());
			System.out.println("--------------------");
			System.out.println(e.getStackTrace());
			System.out.println("====================");
			// Resume game
			timer.start();
			return false;
		}// end catch
			// Resume game
		timer.start();
		return true;
	}// end method save

	/**
	 * Determines whether or not a file name is valid (without the extension)
	 * 
	 * @param fileName
	 *            The name of the file (without the extension)
	 * @return Whether or not this is a valid file name
	 */
	public boolean validFileName(String fileName) {
		// get rid of extra white space
		fileName = fileName.trim();
		// file names cannot be null and cannot be blank
		if (fileName != null && fileName.length() > 0) {
			// file names must be [1, 15] characters
			if (fileName.length() > 15) {
				JOptionPane.showMessageDialog(null,
						"Error: File names cannot be more than 15 characters.",
						"Invalid File Name", JOptionPane.ERROR_MESSAGE);
				return false;
			}// end if
				// file names can only contain alphanumeric characters
			if (fileName.matches("[0-9a-zA-Z ]+")) {
				return true;
			}// end else if
			else {
				JOptionPane
						.showMessageDialog(
								null,
								"Error: File names must only contain alphanumeric characters.",
								"Invalid File Name", JOptionPane.ERROR_MESSAGE);
				return false;
			}// end else
		}// end if
		return false;
	}// end method validFileName

	@Override
	public int getTurnLength() {
		return currentTurnLength;
	}

	@Override
	public int getRemainingTime() {
		return currentTurnLength - timeCounter;
	}

	@Override
	public RenderablePlayer[] getRankedPlayers() {
		return (RenderablePlayer[]) game.getRankedPlayers();
	}

	@Override
	public RenderableTile getCurrentTile() {
		// Not relevant in this old game driver.
		return null;
	}

}// end class GameDriver
